Skip to content

Restore the MCP tracer config, and trace the api → maple-ai hop - #868

Merged
Makisuo merged 3 commits into
mainfrom
fix/maple-ai-telemetry-and-forward
Sep 12, 2026
Merged

Restore the MCP tracer config, and trace the api → maple-ai hop#868
Makisuo merged 3 commits into
mainfrom
fix/maple-ai-telemetry-and-forward

Conversation

@Makisuo

@Makisuo Makisuo commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #861. Four things the split changed by omission, plus the tests that would have caught two of them.

The MCP tracer config did not move with the surfaces

apps/api passed WorkerTelemetry two options that maple-ai did not:

  • anticipatedErrorIdentifiers: MCP_ANTICIPATED_ERROR_IDENTIFIERS — what keeps an expected 400/401 (a tool call that does not decode, a missing or invalid credential) exporting with an Ok status and no exception event. resolve-tenant.ts documents the mechanism and then relied on a config that no longer set it, so every anticipated MCP rejection was landing in Maple's own error tracking as an unexpected error.
  • dropSpanNames: ["McpServer/Notifications."] — which stayed behind on api, where nothing serves MCP any more.

The existing guard could not see this: it only inspects tracers built through MapleCloudflareSDK.make, and the request-facing one is built by WorkerTelemetry in the Worker init. It now covers that too, and I checked it fails when the line is removed.

maple-ai also gets the TracerDisabledWhen filter the bridge's tracer reads, so a liveness probe does not span. Header redaction stays at Effect's defaults, which already cover every credential reaching this Worker.

The forward carried no trace context

fromCloudflareFetcher calls the binding through a raw fetch and nothing instruments outbound calls, so maple-ai opened a new root trace and one /mcp call read as two unrelated traces with no edge between the Workers on the service map. The forward now replaces traceparent with api's own server span. Everything else still crosses byte for byte, streams included.

The forward had no test, and an inline cast

The path predicate is the contract between the two Workers — a path it misses 404s from api's router, one it over-matches never reaches api's routes at all — and both failures are invisible to a typecheck. It moves to worker/ai-forward.ts with a table in both directions, plus coverage of header, body and host preservation and the injected traceparent. The binding is narrowed by a guard rather than asserted into the fetcher type, so a present-but-wrong binding is a logged 503 instead of a defect inside alchemy's adapter.

The Workflow bound api's database consumer

MapleDb(consumer) picks the Hyperdrive config MAPLE_DB binds on the host script, and the fan-out Workflow still named "api" while its host yields "ai" — two MAPLE_DB binds on maple-ai, harmless only while both resolve to the same config. That is exactly the state the maple-ai-prd TODO exists to end.

Also moves @maple-dev/effect-sdk to dependencies, where chat/turn-runner.ts imports it from production code, and corrects two comments that still name apps/api as the Workflow's host.

Checks

@maple/ai typecheck, apps/api tsc, oxfmt and oxlint on every touched file, and the forward, graph-boundary and expected-failure suites. All green. The repo-wide typecheck and full suite are CI's.

Still not code

Two prd gates from the review remain open: the dedicated maple-ai-prd Hyperdrive config does not exist, so maple-ai serves prd traffic off api's pool, and the "Prod revision skew" alert rule (its SQL lives in the production database) has to list maple-ai.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Makisuo and others added 3 commits September 12, 2026 19:43
…acer

The move out of apps/api dropped both options the api passed to `WorkerTelemetry`,
and nothing tied the two files together. `anticipatedErrorIdentifiers` is what keeps
an expected 400/401 — a tool call that does not decode, a missing or invalid
credential — exporting with an `Ok` status and no exception event, per the rule that
only 5xx is an `Error` span; `resolve-tenant.ts` says so in a comment and then relies
on a config that no longer set it. Without it every anticipated MCP rejection lands in
Maple's own error tracking as an unexpected error. `dropSpanNames` is the same story
for the MCP server's notification spans, and it stayed behind on api, where nothing
serves MCP any more.

The guard in `expected-failures.test.ts` could not see this: it only inspects tracers
built through `MapleCloudflareSDK.make`, and the request-facing one is built by
`WorkerTelemetry` in the init. It now covers that too, and fails on a `WorkerTelemetry`
call in this app that omits the identifiers.

`AiObservabilityLive` carries the `TracerDisabledWhen` filter the bridge's tracer
reads, so a liveness probe does not span. Header redaction stays at Effect's defaults,
which already cover every credential reaching this Worker — the provider webhook
signatures on api's list are received on api's routes and never forwarded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`fromCloudflareFetcher` calls the binding through a raw `fetch`, and the server SDK
instruments no outbound calls, so the forwarded request left with whatever
`traceparent` the client sent — usually none. maple-ai then opened a new root trace and
one `/mcp` call read as two unrelated traces, with no edge between the Workers on the
service map. The forward now replaces `traceparent` with api's own server span, which
is what parents ai's span to it; a client that sent its own is already that span's
parent, so the trace stays whole either way. Everything else still crosses byte for
byte, streams included.

Moved into its own module for two reasons. The path predicate is the contract between
the two Workers — a path it misses 404s from api's router, one it over-matches never
reaches api's routes at all, and both failures are invisible to a typecheck — and it is
now covered by a table in both directions, including `/mcp-something` and
`/.well-known/oauth-protected-resource/mcp`. And the binding arrives as an unparsed
`env` value, so it is narrowed by a guard rather than asserted into the fetcher type: a
binding that is present but wrong is a logged 503 instead of a defect inside alchemy's
adapter.

A binding call that rejects stays a defect the bridge renders and reports, exactly as
before the split. The only expected failure that is absorbed is a request that cannot
be rendered to the web shape, which on workerd cannot happen — the bridge's request
already is one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fan-out Workflow moved to maple-ai still yielded `MapleDb("api")`, which is the
consumer that picks the Hyperdrive config `MAPLE_DB` binds on the HOST script — so
maple-ai bound `MAPLE_DB` twice, once per consumer. Harmless only while both resolve to
the same config, which is exactly the state the `maple-ai-prd` TODO exists to end.

Also moves `@maple-dev/effect-sdk` to dependencies, where `chat/turn-runner.ts` imports
it from production code, and corrects two comments that still name apps/api as the
Workflow's host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Makisuo
Makisuo merged commit 4e340c0 into main Sep 12, 2026
46 checks passed
@Makisuo
Makisuo deleted the fix/maple-ai-telemetry-and-forward branch September 12, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant